
(function(window, document, $) {

var months = ['January', 'February', 'March', 
              'April', 'May', 'June', 
              'July', 'August', 'September',
              'October', 'November', 'December'],
    days = ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'],
    start_of_week = 1;

    function getMonthNumber(name) {
        for (var i=0; i<months.length; ++i)
            if (months[i].toLowerCase().substr(0, name.length) == name.toLowerCase())
                return i + 1;
        return 0; 
    }

    function parseDate(val) {
        var re = /^(\d\d?)[ \.\/-](\d\d?|[\w]{2,10})([ \.\/-](\d\d(\d\d)?))?$/,
            match = val.match(re);

        if (!match)
            return null;
        
        var d = parseInt(match[1], 10),
            m = match[2],
            y = match[4],
            result;

        if (y)
            y = parseInt(y, 10);
        else
            y = (new Date()).getFullYear();

        if (m.search(/[0-9]{1,2}/) == 0)
            m = parseInt(m, 10);
        else if (m.search(/[\w]{2,10}/) == 0)
            m = getMonthNumber(m);
        else
            return null;

        if (m < 1 || m > 12)
            return null;

        if (y < 100)
            y += 2000;
        if (y > (new Date()).getFullYear() + 5)
            y -= 100;
        if (y < 1900)
            return null;
        result = new Date(y, m-1, d, 12, 0);
        if (result.getDate() == d
            && result.getMonth() + 1 == m
            && result.getFullYear() == y)
            return result;
        return null;
    }


function DateInput(el, args) {
    var self = this;

    self.getValue = args.getValue || self.getValue;
    self.setValue = args.setValue || self.setValue;
   
    function bind(f) {
        return function(){return f.apply(self, arguments)};
    }
        
    self.inputFocused = bind(self.inputFocused);
    self.inputChanged = bind(self.inputChanged);
    self.inputKeyDown = bind(self.inputKeyDown);
    self.inputMouseDown = bind(self.inputMouseDown);
    self.bindHide = bind(self.bindHide);
    self.bindShow = bind(self.bindShow);
    self.bodyClicked = bind(self.bodyClicked);
    self.input = $(el).data('date_input_control', self);
    self.bindInit();
};


DateInput.prototype = {

    __built: false,
    build: function() {
        var self = this;
        if (this.__built)
            return;
        this.__built = true;
        var monthNav = $('<p class="month-nav">'
            + '<span class="button prev">&#171;</span>'
            + ' <span class="month-name"></span> '
            + '<span class="button next">&#187;</span>'
            + '</p>');
        this.monthNameSpan = $('.month-name', monthNav);
        $('.prev', monthNav).click(function(){self.navigate(-1);return false});
        $('.next', monthNav).click(function(){self.navigate(1);return false});
        
        var yearNav = $('<p class="year-nav">'
            + '<span class="button prev" title="Hold shift to move 10">&#171;</span>'
            + ' <span class="year-name"></span> '
            + '<span class="button next" title="Hold shift to move 10">&#187;</span>'
            + '</p>');
        this.yearNameSpan = $('.year-name', yearNav);
        $('.prev', yearNav).click(function(e){self.navigate(-12 * (e.shiftKey?10:1));return false});
        $('.next', yearNav).click(function(e){self.navigate(12 * (e.shiftKey?10:1));return false});
        
        var nav = $('<div class="nav"></div>').append(monthNav, yearNav);
        var tableShell = '<table><thead><tr>';
        $(days).each(function() {
            tableShell += '<th>' + this + '</th>';
        });
        tableShell += '</tr></thead><tbody></tbody></table>';
        
        this.dateSelector = this.root = $('<div class="date-selector"></div>')
            .append(nav, tableShell)
            .insertBefore(this.input);
        
        if ($.browser.msie && $.browser.version < 7) {
            // The ieframe is a hack which works around an IE <= 6 bug where absolutely positioned elements
            // appear behind select boxes. Putting an iframe over the top of the select box prevents this.
            this.ieframe = $('<iframe class="date-selector-ieframe" frameborder="0" src="javascript:false"></iframe>')
                            .insertBefore(this.dateSelector);
            this.root = this.root.add(this.ieframe);
        };
        
        this.tbody = $('tbody', this.dateSelector);
        $('div.body-panel div.scroll').scroll(this.hide);
    },

    setValue: function(value) {
        if (!value)
            this.input.val('');
        this.input.data('dateValue', value);
    },

    getValue: function() {
        return this.input.data('dateValue');
    },
    
    formatDate: function(d) {
        return d.getDate() + ' ' + months[d.getMonth()].substr(0, 3) + ' ' + d.getFullYear();
    },
    
    encodeDate: function(d) {
        return [d.getFullYear(), d.getMonth(), d.getDate()].join('-');
    },

    decodeDate: function(d) {
        d = d.split('-');
        return new Date(d[0], d[1], d[2]);
    },

    visible: false,

    bindInit: function() {
        this.input.bind('change', this.inputChanged);
        this.input.bind('focus', this.inputFocused);
        this.input.bind('key'+($.browser.msie ?'down':'press'), this.inputKeyDown);
    },

    bindShow: function() {
        $(document).bind('click', this.bodyClicked);
        this.input.unbind('focus', this.inputFocused);
        this.input.bind('mousedown', this.inputMouseDown);
    },

    bindHide: function() {
        $(document).unbind('click', this.bodyClicked);
        this.input.unbind('mousedown', this.inputMouseDown);
        this.input.bind('focus', this.inputFocused);
    },

    show: function() {
        if (!this.__built)
            this.build();
        this.setDisplayedMonth(this.getValue() || new Date());
        this.root.css('display', 'block');
        this.setPosition();
        this.bindShow();
        this.visible = true;
    },

    hide: function() {
        this.visible = false;
        this.root.css('display', 'none');
        setTimeout(this.bindHide, 10)
    },
    
    toggle: function() {
        if (this.visible)
            this.hide();
        else
            this.show();
    },

    redraw: function() {
        var self = this;
        var monthFirst = this.displayedMonth,
            start = this.rangeStart(monthFirst),
            days = this.daysBetween(start, this.rangeEnd(monthFirst)),
            cells = '';

        // Draw each of the days
        for (var i = 0; i <= days; i++) {
            var d = new Date(start.getFullYear(), start.getMonth(), start.getDate() + i, 12, 00);
            
            if (d.getDay() == start_of_week)
                cells += '<tr>';
            
            var day_class = (d.getMonth() == monthFirst.getMonth() 
                                ? 'selectable-day' 
                                : 'unselected-month');
            cells += ('<td class="' 
                    + day_class 
                    + '" date="' 
                    + this.encodeDate(d) 
                    + '">' 
                    + d.getDate() 
                    + '</td>');
            
            if (d.getDay() == (start_of_week - 1) % 7)
                cells += '</tr>';
        };
        this.tbody.empty().append(cells);
        
        // Write the month and year in the header
        this.monthNameSpan.empty().append(months[monthFirst.getMonth()]);
        this.yearNameSpan.empty().append(monthFirst.getFullYear());
        
        $('.selectable-day', this.tbody).click(function(e) {
            var val = self.decodeDate($(e.target).attr('date'));
            self.setValue(val);
            self.input.val(self.formatDate(val));
            self.hide();
        });
        
        $('td[date=' + this.encodeDate(new Date()) + ']', this.tbody).addClass('today');
    },

    bodyClicked: function(e) {
        if (e.target != this.input[0] && this.visible)
            this.hide();
    },
    
    inputFocused: function(e) {
        if (!this.visible)
            this.show();
    },

    inputMouseDown: function(e) {
        this.toggle();
    },

    __keytimer: null,

    inputChanged: function(e) {
        clearTimeout(this.__keytimer);
        var val = this.input.val(),
            parsed = val ? parseDate(val) : null;
        this.input.val(parsed ? this.formatDate(parsed) : '');
        this.setValue(parsed);
    },

    inputKeyDown: function(e) {
        clearTimeout(this.__keytimer);
        this.__keytimer = setTimeout(this.inputChanged, 5000);
        if (!this.visible)
            return;
        switch (e.keyCode)
        {
            case 9: // tab
            case 27: // esc
                this.hide(); 
                return;
            case 13: // enter
                this.hide();
                break;
            case 16: // shift
                return;
            /*
            case 38: // up
                this.changeDisplayedDate(-7);
                break;
            case 40: // down
                this.changeDisplayedDate(7);
                break;
            case 33: // page up
                this.changeDisplayedMonth(e.shiftKey ? -12 : -1);
                break;
            case 34: // page down
                this.changeDisplayedMonth(e.shiftKey ? 12 : 1);
                break;
            */
            default:
                this.hide();
                return;
        }
        e.preventDefault();
        return false;
    },


    setDisplayedMonth: function(d) {
        var dm = this.displayedMonth,
            nm = new Date(d.getFullYear(), d.getMonth(), 1),
            changed = (!dm || !(dm.getFullYear == nm.getFullYear()
                                && dm.getMonth() == nm.getMonth()));

        this.displayedMonth = nm;
        if (changed)
            this.redraw();
        this.focusDate(d);
    },

    focusDate: function(d) {
        $('.selected', this.tbody).removeClass('selected');
        $('td[date=' + this.encodeDate(d) + ']', this.tbody).addClass('selected');
    },
    
    setPosition: function() {
        var offset = this.input.position();
        this.root.css({
            position: 'absolute',
            top: offset.top + this.input.outerHeight() + 'px',
            left: offset.left + 'px'
        });

        if (this.ieframe) {
            this.ieframe.css({
                width: this.dateSelector.outerWidth(),
                height: this.dateSelector.outerHeight()
            });
        };
    },
    
    changeDisplayedDate: function(deltaDays) {
        var d = this.getValue();
        this._set(new Date(d.getFullYear(), d.getMonth(), d.getDate() + deltaDays));
    },
    
    /*
    changeDisplayedMonth: function(delta) {
        var d1 = this.getValue(),
            d2 = new Date(d1.getFullYear(), d1.getMonth() + delta, d1.getDate());
        if (d2.getMonth() > d1.getMonth() + delta)
            d2.setDate(0);
        this._set(d2);
    },
    */

    navigate: function(monthDelta) {
        var dm = this.displayedMonth,
            nm = new Date(dm.getFullYear(),
                          dm.getMonth() + monthDelta,
                          1);
        if (nm.getFullYear() < 1900)
            return;
        this.setDisplayedMonth(nm);
    },

    // internal utility
    // works out the number of days between two dates
    daysBetween: function(start, end) {
        start = Date.UTC(start.getFullYear(), start.getMonth(), start.getDate());
        end = Date.UTC(end.getFullYear(), end.getMonth(), end.getDate());
        return (end - start) / 86400000;
    },
    
    nextDayInDirection: function(dow, d, dir) {
        var delta = dir * (Math.abs(d.getDay() - dow - (dir * 7)) % 7);
        return new Date(d.getFullYear(), d.getMonth(), d.getDate() + delta);
    },
    
    // Given a date, return the day at the start of the week *before* this month
    rangeStart: function(date) {
        return this.nextDayInDirection(start_of_week, new Date(date.getFullYear(), date.getMonth()), -1);
    },
    
    // Given a date, return the day at the end of the week *after* this month
    rangeEnd: function(date) {
        return this.nextDayInDirection((start_of_week - 1) % 7, new Date(date.getFullYear(), date.getMonth() + 1, 0), 1);
    }

};

$.fn.date_input = function(args) {
    this.each(function(){new DateInput(this,args);});
    return this;
};

})(this, this.document, jQuery);

